<?php
class TemplateEngine{
	
	public static $_messages = array();
	
	/**
	 * Whether to store compiled php code or not (for debug purpose)
	 *
	 * @var bool
	 */
	public $reuse = true;
	
	/**
	 * The cache life time.
	 *
	 * @var integer 0 is to disable this feature
	 */
	public $cache = 0;
	
	/**
	 * Directory where all templates are stored
	 *
	 * @access public
	 */
	private $template_dir = CMSPAD_TEMPLATE_PATH;
	
	/**
	 * Where to store compiled templates
	 * 
	 * @access public
	 */
	private $temporary_dir = CMSPAD_TEMPORARY_ABSPATH;
	
	/**
	 * Template variables
	 *
	 * @var array
	 */
	private $variables = array();
	
	/**
	 * Template system functions
	 *
	 * @var array
	 */
	private $functions = array();
	
	/**
	 * Parser Class
	 *
	 * @var TemplateCompiler
	 */
	private $compiler;
	
	/**
	 * TemplateEngine Constructor
	 *
	 * @param string $tpldir Template home directory
	 * @param string $tmpdir Template compiled directory
	 * @param integer $cache cache file life time. 0 to disable.
	 */
	public function __construct($tpldir = '',$tmpdir = '',$cache = 0){
		$this->functions['sys'] = array();
		$this->functions['ext'] = array();
		$this->functions['tag'] = array();
		
		if($tmpdir){
			$this->temporary_dir = $tmpdir;
		}else{
			$tmpath = CMSPAD_TEMPORARY_ABSPATH . 'tpl/';
			if(!file_exists($tmpath)){
				if(!mkdir($tmpath,0777)){
					$tmpath = CMSPAD_TEMPORARY_ABSPATH;
				}
			}
			$this->temporary_dir = $tmpath;
		}
		if($tpldir){
			$this->template_dir = $tpldir;
		}else{
			$this->template_dir = CMSPAD_TEMPLATE_PATH;
		}
		
		$this->cache = intval($cache);
	}
	
	public function getTemplateDirectory(){
		return $this->template_dir;
	}
	
	public function getTemporaryDirectory(){
		return $this->temporary_dir;
	}
	
	/**
	 * Add a template variable.
	 * <p>
	 * Usage Example:
	 * $template->addVariable('TITLE', 'My Document Title');
	 * $template->addVariable('userlist', array(
	 * 																			array( 'ID' => 123,  'NAME' => 'John Doe' ),
	 * 																			array( 'ID' => 124,  'NAME' => 'Jack Doe' ),
	 *                             				);
	 * </p>
	 *
	 * @param string $name Parameter Name
	 * @param mixed $value Parameter Value
	 * @desc Assign Template Content
	 */
	public function addVariable($name,$value = ''){
		if(substr($name,0,1) == '_'){
			trigger_error('Cmspad Template Warning: Variable name(' . $name . ') can not start with "_"',E_USER_WARNING);
			return;
		}
		if(is_array($name)){
			foreach($name as $k=>$v){
				$this->variables[$k] = $v;
			}
		}else{
			$this->variables[$name] = $value;
		}
	}
	
	/**
	 * Add a extended template function.
	 * <b>Note:</b> It can replace default function if they are the same name.
	 * <p>
	 * Usage Example:
	 * $template->addExtFunction('replace', 'str_replace');  // Use global function.
	 * $template->addExtFunction('customFunc', array(&$obj, 'systemFunction'); // Use object method.
	 * </p>
	 *
	 * @param string $name
	 * @param string|array $func
	 * @param bool $addToSys Also add it to system function list
	 */
	public function addExtFunction($name,$func,$addToSys = false){
		if(is_array($name)){
			foreach($name as $k=>$v){
				if(substr($k,0,1) == '_'){
					trigger_error('Cmspad Template Warning: Function name(' . $name . ') can not start with "_"',E_USER_WARNING);
					return;
				}
				$this->functions['ext'][$k] = $v;
			}
		}else{
			if(substr($name,0,1) == '_'){
				trigger_error('Cmspad Template Warning: Function name(' . $name . ') can not start with "_"',E_USER_WARNING);
				return;
			}
			$this->functions['ext'][$name] = $func;
		}
		if($addToSys){
			$this->addSysFunction($name,$func);
		}
	}
	
	/**
	 * Enter description here...
	 *
	 * @param string $name System function name
	 * @return string|array
	 */
	public function getExtFunction($name){
		return isset($this->functions['ext'][$name])?$this->functions['ext'][$name]:false;
	}
	
	/**
	 * Add a system template function.
	 * <b>Note:</b> It can replace default function if they are the same name.
	 * <p>
	 * Usage Example:
	 * $template->addSysFunction('replace', 'str_replace');  // Use global function.
	 * $template->addSysFunction('customFunc', array(&$obj, 'systemFunction'); // Use object method.
	 * </p>
	 *
	 * @param string $name
	 * @param string|array $func
	 */
	public function addSysFunction($name,$func){
		if(is_array($name)){
			foreach($name as $k=>$v){
				if(substr($k,0,1) == '_'){
					trigger_error('Cmspad Template Warning: Function name(' . $name . ') can not start with "_"',E_USER_WARNING);
					return;
				}
				$this->functions['sys'][$k] = $v;
			}
		}else{
			if(substr($name,0,1) == '_'){
				trigger_error('Cmspad Template Warning: Function name(' . $name . ') can not start with "_"',E_USER_WARNING);
				return;
			}
			$this->functions['sys'][$name] = $func;
		}
	}
	
	/**
	 * Enter description here...
	 *
	 * @param string $name System function name
	 * @return string|array
	 */
	public function getSysFunction($name){
		return isset($this->functions['sys'][$name])?$this->functions['sys'][$name]:false;
	}
	
	/**
	 * Add a smarty-like template function.
	 * <p>
	 * Usage Example:
	 * $template->addTagFunction('stringReplace', 'cStringReplace');  // Use global function.
	 * $template->addTagFunction('customFunc', array(&$obj, 'customFunction'); // Use object method.
	 * </p>
	 * <p>
	 * Function Definition:
	 * public function cStringReplace($params){
	 *  // $params is a key/value pairs array object contains arguments passed from tag.
	 * }
	 * </p>
	 *
	 * @param string $name
	 * @param string|array $func
	 */
	public function addTagFunction($name,$func){
		if(is_array($name)){
			foreach($name as $k=>$v){
				if(substr($k,0,1) == '_'){
					trigger_error('Cmspad Template Warning: Function name(' . $name . ') can not start with "_"',E_USER_WARNING);
					return;
				}
				$this->functions['tag'][$k] = $v;
			}
		}else{
			if(substr($name,0,1) == '_'){
				trigger_error('Cmspad Template Warning: Function name(' . $name . ') can not start with "_"',E_USER_WARNING);
				return;
			}
			$this->functions['tag'][$name] = $func;
		}
	}
	
	/**
	 * Enter description here...
	 *
	 * @param string $name
	 * @return string|array
	 */
	public function getTagFunction($name){
		return isset($this->functions['tag'][$name])?$this->functions['tag'][$name]:false;
	}
	
	/**
	 * Clear all variables.
	 *
	 */
	public function clearVariables(){
		$this->variables = array();
	}
	
	public function getVariables(){
		return $this->variables;
	}
	
	public function getVariable($key){
		return isset($this->variables[$key])?$this->variables[$key]:null;
	}
	
	public function getFunctions($type = null){
		switch($type){
			case 'sys':
			case 'ext':
			case 'tag':
				return $this->functions[$type];
			default:
				return $this->functions;
		}
	}
	
	/**
	 * Parser Wrapper
	 * Returns Template Output as a String
	 *
	 * @param string $file template file
	 * @return string  Parsed Template
	 * @desc Output Buffer Parser Wrapper
	 */
	public function fetch($file){
		
		// If use cache, read cached file first.
		if($this->cache){
			//	Make sure that folder names have a trailing '/'
			if(strlen($this->template_dir) && substr($this->template_dir,-1) != '/'){
				$this->template_dir .= '/';
			}
			if(strlen($this->temporary_dir) && substr($this->temporary_dir,-1) != '/'){
				$this->temporary_dir .= '/';
			}
			$cpl_file_name = $this->temporary_dir . md5($this->template_dir . $file) . '.htm';
			if(file_exists($cpl_file_name) && filemtime($cpl_file_name) + $this->cache > time()){
				return file_get_contents($cpl_file_name);
			}
		}
		
		ob_start();
		$spentTime = $this->output($file);
		$result = ob_get_contents();
		ob_end_clean();
		
		// If use cache, save contents into cached file.
		if($this->cache){
			if(($hd = fopen($cpl_file_name,"w"))){
				fwrite($hd,$result);
				fclose($hd);
			}else{
				trigger_error('Cmspad Template Warning: Can not cache template file: ' . $file,E_USER_WARNING);
			}
		}
		
		// Debug info
		$msgs = array();
		$msgs['type'] = 'load';
		$msgs['file'] = $file;
		$msgs['directory'] = $this->template_dir;
		$msgs['time'] = $spentTime;
		$msgs['functions'] = $this->functions;
		$msgs['variables'] = $this->variables;
		
		self::$_messages[microtime()] = $msgs;
		
		return $result;
	}
	
	/**
	 * Get debug messages.
	 * <p>It exports a HTML codes integration for CMSPAD.</p>
	 *
	 * @param string $file debug template file
	 * @return string
	 */
	public static function debug(){
		if(self::$_messages){
			$debug = new TemplateEngine(dirname(__FILE__));
			$debug->cache = 0;
			$debug->addVariable('messages',self::$_messages);
			return $debug->fetch('TemplateDebugger.tpl');
		}else{
			return '';
		}
	}
	
	/**
	 * Execute parsed Template
	 * Prints Parsing Results to Standard Output
	 *
	 * @access public
	 * @param array $_top Content Array
	 * @desc Execute parsed Template
	 */
	public function output($file){
		//	Make sure that folder names have a trailing '/'
		if(strlen($this->template_dir) && substr($this->template_dir,-1) != '/'){
			$this->template_dir .= '/';
		}
		if(strlen($this->temporary_dir) && substr($this->temporary_dir,-1) != '/'){
			$this->temporary_dir .= '/';
		}
		//	Prepare Template Content
		if(!$file){
			exit("Cmspad Template Error: Template file not specified");
		}
		if(!$file && !file_exists($this->template_dir . $file)){
			exit("Cmspad Template Error: Template file not found: " . $this->template_dir . $file);
		}
		
		$time_start = $this->stime();
		
		// Get the cached file
		$file_name = $this->temporary_dir . md5($this->template_dir . $file);
		if($this->cache){
			$cpl_file_name = $file_name . '.htm';
			if(file_exists($cpl_file_name) && filemtime($cpl_file_name) + $this->cache > time()){
				if(@readfile($cpl_file_name)){
					return $this->stime() - $time_start;
				}
			}
		}
		
		$_variables = &$this->variables;
		$_functions = &$this->functions;
		if(!$_functions){
			$_functions = array();
		}
		$_stack_cnt = 0;
		$_stack = array();
		$_stack[$_stack_cnt++] = $_variables;
		
		//	Check if template is already compiled
		$cfile = $file_name . '.php';
		$compile_template = true;
		if($this->reuse){
			if(is_file($cfile)){
				if($this->mtime($cfile) > $this->mtime($this->template_dir . $file)){
					$compile_template = false;
				}
			}
		}
		if($compile_template){
			if(@include_once (dirname(__FILE__) . DIRECTORY_SEPARATOR . "TemplateCompiler.php")){
				$this->compiler = new TemplateCompiler($this,$this->template_dir . $file);
				if(!$this->compiler->compile($cfile)){
					exit("Cmspad Template Error: " . $this->compiler->error);
				}
			}else{
				exit("Cmspad Template Error: Cannot find TemplateCompiler.php; check cmspad template installation");
			}
		}
		include($cfile);
		return $this->stime() - $time_start;
	}
	
	/**
	 * Determine Last Filechange Date (if File exists)
	 *
	 * @access private
	 * @param string $filename
	 * @return mixed
	 * @desc Determine Last Filechange Date
	 */
	private function mtime($filename){
		if(@is_file($filename)){
			$ret = filemtime($filename);
			return $ret;
		}else{
			return false;
		}
	}
	
	private function stime(){
		list($usec,$sec) = explode(" ",microtime());
		return ((float)$usec + (float)$sec);
	}
}
?>